/*
 * This file is part of NativeAccessHooks.
 *
 * Copyleft 2014 Mark Jeronimus. All Rights Reversed.
 * https://code.google.com/p/nativeaccesshooks/
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with NativeAccessHooks. If not, see <http://www.gnu.org/licenses/>.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package com.digitalmodular.nah.mouse;

import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

import com.digitalmodular.nah.HookManager;
import com.digitalmodular.nah.NativeHook;
import com.digitalmodular.nah.NativeInputEvent;
import com.digitalmodular.nah.mouse.NativeMouseWheelEvent.NativeMouseWheel;
import com.digitalmodular.nah.platform.win32.WinUserMJ;
import com.digitalmodular.nah.platform.win32.WinUserMJ.LowLevelMouseProc;
import com.digitalmodular.nah.platform.win32.WinUserMJ.MSLLHOOKSTRUCT;
import com.sun.jna.platform.win32.Kernel32;
import com.sun.jna.platform.win32.User32;
import com.sun.jna.platform.win32.WinDef.HINSTANCE;
import com.sun.jna.platform.win32.WinDef.LRESULT;
import com.sun.jna.platform.win32.WinDef.WPARAM;
import com.sun.jna.platform.win32.WinUser.HHOOK;
import com.sun.jna.platform.win32.WinUser.HOOKPROC;
import com.sun.jna.platform.win32.WinUser.MSG;

/**
 * The hook that receives platform-wide mouse messages. When a message is received, it's put into a queue with a timestamp attached.
 * A separate thread monitors the queue, formulates {@link NativeInputEvent} events and dispatches them through
 * {@link HookManager#dispatchEvent(com.digitalmodular.nah.NativeInputEvent)};
 *
 * @author Mark Jeronimus
 * @since 1.0
 */
// date 2014-08-22
public class NativeMouseHook extends NativeHook implements Runnable {
	final LinkedBlockingQueue<MousePostMessage>	buffer		= new LinkedBlockingQueue<>();

	HHOOK										hhk			= null;

	// Mutex:
	ReentrantLock								lock		= new ReentrantLock();
	Condition									condition	= lock.newCondition();

	public NativeMouseHook() {
		new Thread(getClass().getSimpleName()) {
			@Override
			public void run() {
				final HOOKPROC lpfn = new LowLevelMouseProc() {
					@Override
					public LRESULT callback(final int nCode, final WPARAM wParam, final MSLLHOOKSTRUCT lParam) {
						if (nCode >= 0) {
							// System.out.println("nCode " + nCode + " wParam " + Integer.toHexString(wParam.intValue()) + " x="
							// + lParam.pt.x + " y=" + lParam.pt.y + " mouseData=" + Integer.toHexString(lParam.mouseData)
							// + " time=" + lParam.time + " dwExtraInfo=" + Long.toHexString(lParam.dwExtraInfo.longValue()));

							lock.lock();
							try {
								buffer.offer(new MousePostMessage(System.currentTimeMillis(), System.nanoTime(), wParam, lParam));
								condition.signalAll();
							}
							finally {
								lock.unlock();
							}
						}
						return User32.INSTANCE.CallNextHookEx(hhk, nCode, wParam, lParam.getPointer());
					}
				};

				synchronized (REGISTER_MUTEX) {
					final HINSTANCE hMod = Kernel32.INSTANCE.GetModuleHandle(null);

					hhk = User32.INSTANCE.SetWindowsHookEx(com.sun.jna.platform.win32.WinUser.WH_MOUSE_LL, lpfn, hMod, 0);

					REGISTER_MUTEX.notifyAll();

					if (hhk == null) {
						System.out.println("Failed to register " + NativeMouseHook.class.getSimpleName() + ".");

						return;
					}

					System.out.println(NativeMouseHook.class.getSimpleName() + " hook registered.");
				}

				try {
					int result;
					final MSG msg = new MSG();
					while ((result = User32.INSTANCE.GetMessage(msg, null, 0, 0)) != 0) {
						if (result == -1) {
							unregisterHook();
							return;
						}
						User32.INSTANCE.TranslateMessage(msg);
						User32.INSTANCE.DispatchMessage(msg);
					}
				}
				finally {
					unregisterHook();
				}
			}
		}.start();

		synchronized (REGISTER_MUTEX) {
			if (hhk == null) {
				try {
					REGISTER_MUTEX.wait();
				}
				catch (InterruptedException e) {}
			}
		}

		new Thread(this, getClass().getSimpleName() + " dispatch thread").start();
	}

	@Override
	public boolean isRegistered() {
		return hhk != null;
	}

	@Override
	public void unregisterHook() {
		User32.INSTANCE.UnhookWindowsHookEx(hhk);
		hhk = null;

		System.out.println(NativeMouseHook.class.getSimpleName() + " hook unregistered.");
	}

	private static class MousePostMessage {
		public final long			time;
		public final long			nanoTime;
		public final WPARAM			wParam;
		public final MSLLHOOKSTRUCT	lParam;

		public MousePostMessage(final long time, final long nanoTime, final WPARAM wParam, final MSLLHOOKSTRUCT lParam) {
			this.time = time;
			this.nanoTime = nanoTime;
			this.wParam = wParam;
			this.lParam = lParam;
		}
	}

	@Override
	public void run() {
		try {
			while (true) {
				lock.lock();
				try {
					condition.await();
				}
				finally {
					lock.unlock();
				}

				MousePostMessage message;
				while ((message = buffer.poll()) != null) {
					long time = message.time;
					long nanoTime = message.nanoTime;
					int wParam = message.wParam.intValue();
					int mouseData = message.lParam.mouseData >> 16;

					switch (wParam) {
						case WinUserMJ.MOUSEMOVE: //
							HookManager.dispatchEvent(new NativeMouseMotionEvent(this, time, nanoTime, message.lParam.pt.x,
									message.lParam.pt.y));
							break;
						case WinUserMJ.LBUTTONDOWN: //
							HookManager.dispatchEvent(new NativeMouseButtonEvent(this, time, nanoTime, message.lParam.pt.x,
									message.lParam.pt.y, NativeMouseButtonEvent.NativeMouseButtonEventType.ButtonPressed,
									NativeMouseButton.Left));
							break;
						case WinUserMJ.LBUTTONUP: //
							HookManager.dispatchEvent(new NativeMouseButtonEvent(this, time, nanoTime, message.lParam.pt.x,
									message.lParam.pt.y, NativeMouseButtonEvent.NativeMouseButtonEventType.ButtonReleased,
									NativeMouseButton.Left));
							break;
						case WinUserMJ.RBUTTONDOWN: //
							HookManager.dispatchEvent(new NativeMouseButtonEvent(this, time, nanoTime, message.lParam.pt.x,
									message.lParam.pt.y, NativeMouseButtonEvent.NativeMouseButtonEventType.ButtonPressed,
									NativeMouseButton.Right));
							break;
						case WinUserMJ.RBUTTONUP: //
							HookManager.dispatchEvent(new NativeMouseButtonEvent(this, time, nanoTime, message.lParam.pt.x,
									message.lParam.pt.y, NativeMouseButtonEvent.NativeMouseButtonEventType.ButtonReleased,
									NativeMouseButton.Right));
							break;
						case WinUserMJ.MBUTTONDOWN: //
							HookManager.dispatchEvent(new NativeMouseButtonEvent(this, time, nanoTime, message.lParam.pt.x,
									message.lParam.pt.y, NativeMouseButtonEvent.NativeMouseButtonEventType.ButtonPressed,
									NativeMouseButton.Middle));
							break;
						case WinUserMJ.MBUTTONUP: //
							HookManager.dispatchEvent(new NativeMouseButtonEvent(this, time, nanoTime, message.lParam.pt.x,
									message.lParam.pt.y, NativeMouseButtonEvent.NativeMouseButtonEventType.ButtonReleased,
									NativeMouseButton.Middle));
							break;
						case WinUserMJ.MOUSEWHEEL: //
							HookManager.dispatchEvent(new NativeMouseWheelEvent(this, time, nanoTime, message.lParam.pt.x,
									message.lParam.pt.y, NativeMouseWheel.Vertical, mouseData / 120));
							break;
						case WinUserMJ.XBUTTONDOWN: //
							HookManager.dispatchEvent(new NativeMouseButtonEvent(this, time, nanoTime, message.lParam.pt.x,
									message.lParam.pt.y, NativeMouseButtonEvent.NativeMouseButtonEventType.ButtonPressed,
									NativeMouseButton.values()[NativeMouseButton.Middle.ordinal() + mouseData]));
							break;
						case WinUserMJ.XBUTTONUP: //
							HookManager.dispatchEvent(new NativeMouseButtonEvent(this, time, nanoTime, message.lParam.pt.x,
									message.lParam.pt.y, NativeMouseButtonEvent.NativeMouseButtonEventType.ButtonReleased,
									NativeMouseButton.values()[NativeMouseButton.Middle.ordinal() + mouseData]));
							break;
						case WinUserMJ.MOUSEHWHEEL: //
							HookManager.dispatchEvent(new NativeMouseWheelEvent(this, time, nanoTime, message.lParam.pt.x,
									message.lParam.pt.y, NativeMouseWheel.Horizontal, mouseData / 120));
							break;
						default:
							StringBuilder stringBuilder = new StringBuilder(256);
							stringBuilder.append("Unknown mouse event:");
							stringBuilder.append("wParam=0x").append(Integer.toHexString(wParam));
							stringBuilder.append(", mouseData=0x").append(Integer.toHexString(mouseData));
							System.out.println(stringBuilder.toString());
					}
				}
			}
		}
		catch (InterruptedException e) {}
	}
}
